﻿using Afterbunny.ImageMarker.WPF.DTO;
using Afterbunny.UI.WPF.AttachedProperties;
using Afterbunny.UI.WPF.Core;
using Afterbunny.Windows.Helpers;
using CygiaLog.Module;
using CygiaSqliteAccess.Proxy;
using DistributedVisionRunner.Interface;
using DistributedVisionRunner.Interface.Client;
using DistributedVisionRunner.Interface.Model;
using DistributedVisionRunner.Module.Helper;
using DistributedVisionRunner.Module.Models;
using DistributedVisionRunner.Module.Views;
using Dynamitey;
using NetMQ;
using NetMQ.Sockets;
using Newtonsoft.Json;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using Prism.Services.Dialogs;
using Proxy;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Xml.Serialization;
using UniversalWeightSystem.Core;
using Application = System.Windows.Application;

namespace DistributedVisionRunner.Module.ViewModels
{
    public class VisionProcessUnitViewModel<TData> : BindableBase, IVisionProcessUnit
    {
        #region private fields

        private const string UnspecifiedProductName = "UnspecifiedProduct";
        private const string UnspecifiedSN = "UnspecifiedSN";
        private readonly IEventAggregator _ea;
        private readonly IDialogService _dialogService;
        private readonly TypeSource _processorTypeSource;
        private readonly TypeSource _adapterTypeSource;

        private readonly ResponseSocket _serverSocket;
        private ProcessUnitUserSetting _userSetting;
        private string _userSettingPath;
        private string _tableNameRawOnline;
        private string _tableNameWeightedOnline;
        private string _tableNameReservedOnline;
        private string _tableNameRawOffline;
        private string _tableNameWeightedOffline;
        private string _tableNameReservedOffline;
        private bool _databaseServiceInstalled = true;
        private readonly Regex _namingPattern = new Regex(@"_?[a-zA-Z]+[\w_]*");


        #endregion private fields

        #region props

        public IVisionAdapter<TData> Adapter { get; }
        public IVisionProcessor<TData> Processor { get; }
        public bool EnableZeroMQ { get; private set; }


        private List<GraphicPayload> _displayImages;

        public List<GraphicPayload> DisplayImages
        {
            get => _displayImages;
            private set => SetProperty(ref _displayImages, value);
        }

        private ObservableCollection<DynamicClass> _displayData = new ObservableCollection<DynamicClass>();

        public ObservableCollection<DynamicClass> DisplayData
        {
            get => _displayData;
            set => SetProperty(ref _displayData, value);
        }

        public ObservableCollection<DynamicClass> RawData
        {
            get => _rawData;
            set => SetProperty(ref _rawData, value);
        }

        public ObservableCollection<DynamicClass> WeightedData
        {
            get => _weightedData;
            set => SetProperty(ref _weightedData, value);
        }

        public ObservableCollection<DynamicClass> ReservedData
        {
            get => _reservedData;
            set => SetProperty(ref _reservedData, value);
        }


        public bool IsIdle
        {
            get => _isIdle;
            set => SetProperty(ref _isIdle, value);
        }

        private uint _busyCounter = 0;

        private bool _isIdle = true;
        private readonly object _busyCounterLock = new object();
        private bool _weightsConfigured;
        private string _weightProjectDir;
        private string _weightProjectFilePath;
        private Dictionary<int, Dictionary<string, double>> _weightCollectionByCavity;
        private Dictionary<string, string> _methodsByOutputName;
        private HashSet<string> _adapterFloatNamesLookup;
        private HashSet<string> _adapterIntegerNamesLookup;
        private HashSet<string> _adapterTextNamesLookup;
        private HashSet<string> _processorOutputNamesLookup;
        private readonly bool _noNamingProblems;
        private bool _isShuttingDown;
        private ManualResetEvent _zeroMQServerIsListeningBlocker;
        private bool _visionConfigFilesLoadFailed;
        private string _userSettingDir;
        private readonly string _processorConfigFileDir;
        private ObservableCollection<DynamicClass> _rawData = new ObservableCollection<DynamicClass>();
        private ObservableCollection<DynamicClass> _weightedData = new ObservableCollection<DynamicClass>();
        private ObservableCollection<DynamicClass> _reservedData = new ObservableCollection<DynamicClass>();
        private List<BoundaryItem> _rawBoundaries;
        private List<BoundaryItem> _weightedBoundaries;
        private List<BoundaryItem> _reservedBoundaries;

        /// <summary>
        /// Dictionary to boost look up performance
        /// </summary>
        private Dictionary<string, BoundaryItem> _rawBoundariesLookupTable;
        private Dictionary<string, BoundaryItem> _weightedBoundariesLookupTable;
        private Dictionary<string, BoundaryItem> _reservedBoundariesLookupTable;
        private readonly bool _noDelimiterProblems;
        private Dictionary<string, int> _weightedDataOrderDict;
        private Dictionary<string, int> _reservedDataOrderDict;
        private Dictionary<string, int> _rawDataOrderDict;
        private ColumnBinding[] _rawDataColumnBindings;
        private ColumnBinding[] _weightedDataColumnBindings;
        private ColumnBinding[] _reservedDataColumnBindings;
        private readonly object _dbLocker = new object();
        private bool _shouldShowTable;
        private ImageLayoutParameter _imagesLayout;

        public bool WeightsConfigured
        {
            get => _weightsConfigured;
            set => SetProperty(ref _weightsConfigured, value);
        }

        public ColumnBinding[] RawDataColumnBindings
        {
            get => _rawDataColumnBindings;
            set => SetProperty(ref _rawDataColumnBindings, value);
        }

        public ColumnBinding[] WeightedDataColumnBindings
        {
            get => _weightedDataColumnBindings;
            set => SetProperty(ref _weightedDataColumnBindings, value);
        }

        public ColumnBinding[] ReservedDataColumnBindings
        {
            get => _reservedDataColumnBindings;
            set => SetProperty(ref _reservedDataColumnBindings, value);
        }

        public bool ShouldShowTable
        {
            get => _shouldShowTable;
            private set => SetProperty(ref _shouldShowTable, value);
        }

        public string Name { get; }

        public string ServerAddress { get; }

        public string ImageSaveFolderToday => Path.Combine(_userSetting.ImageSaveMainFolder, DateTime.Now.ToString("yyyy-MM-dd"));

        public ICommand ShowPropertiesCommand { get; }
        public ICommand OpenBoundarySettingDialogCommand { get; }
        public ICommand RunSingleFileCommand { get; }
        public ICommand RunFolderCommand { get; }
        public ICommand OpenSettingDialogCommand { get; }

        public ICommand OpenImageLayoutSettingDialogCommand =>

            new DelegateCommand(() =>
            {
                var dialog = new ImageLayoutSettingDialog(_userSetting.ImagePlacementMethod, _userSetting.ImagesPerRow, _userSetting.ImageItemWidth, _userSetting.ImageItemHeight)
                {
                    WindowStartupLocation = WindowStartupLocation.CenterOwner,
                    Owner = Application.Current.MainWindow
                };
                if (dialog.ShowDialog() == true)
                {
                    _userSetting.ImageItemHeight = dialog.ImageHeight;
                    _userSetting.ImageItemWidth = dialog.ImageWidth;
                    _userSetting.ImagePlacementMethod = dialog.SelectedPlacement;
                    _userSetting.ImagesPerRow = dialog.ImagesPerRow;

                    UpdateImagesLayoutFromSetting();

                    SaveUserSetting(_userSetting);
                }
            });

        private void UpdateImagesLayoutFromSetting()
        {
            var placement = _userSetting.ImagePlacementMethod;
            var width = placement == ItemPlacementMethod.UniformGrid ? double.NaN : _userSetting.ImageItemWidth;
            var height = placement == ItemPlacementMethod.UniformGrid ? double.NaN : _userSetting.ImageItemHeight;


            ImagesLayout = new ImageLayoutParameter()
            {
                ImageItemHeight = height,
                ImageItemWidth = width,
                PlacementMethod = placement,
                ImagesPerRow = _userSetting.ImagesPerRow
            };
        }

        public ImageLayoutParameter ImagesLayout
        {
            get => _imagesLayout;
            private set => SetProperty(ref _imagesLayout, value);
        }

        public ICommand OpenWeightEditorDialogCommand { get; }
        public ICommand ClearDisplayDataCommand { get; }
        public ICommand SwitchDisplayDataCommand { get; }
        public ICommand ShowTableCommand => new DelegateCommand<bool?>(showTable =>
        {
            var shouldShow = showTable == true;
            ShouldShowTable = shouldShow;
            _userSetting.ShouldShowTable = shouldShow;
            SaveUserSetting(_userSetting);
        });

        public uint BusyCounter
        {
            get
            {
                lock (_busyCounterLock)
                {
                    var output = _busyCounter;
                    return output;
                }
            }
            set
            {
                lock (_busyCounterLock)
                {
                    _busyCounter = value;
                }
            }
        }

        public List<BoundaryItem> RawBoundaries
        {
            get => _rawBoundaries;
            set
            {
                _rawBoundaries = value;
                GenBoundaryLookupTable(value, ref _rawBoundariesLookupTable);
            }
        }


        public List<BoundaryItem> WeightedBoundaries
        {
            get => _weightedBoundaries;
            set
            {
                _weightedBoundaries = value;
                GenBoundaryLookupTable(value, ref _weightedBoundariesLookupTable);
            }
        }

        public List<BoundaryItem> ReservedBoundaries
        {
            get => _reservedBoundaries;
            set
            {
                _reservedBoundaries = value;
                GenBoundaryLookupTable(value, ref _reservedBoundariesLookupTable);
            }
        }

        #endregion props

        #region ctor

        public VisionProcessUnitViewModel(IEventAggregator ea, IDialogService dialogService,
            TypeSource processorTypeSource, TypeSource adapterTypeSource)
        {
            _ea = ea;
            _dialogService = dialogService;
            _processorTypeSource = processorTypeSource;
            _adapterTypeSource = adapterTypeSource;
            Processor = (IVisionProcessor<TData>)Activator.CreateInstance(processorTypeSource.Type);
            Adapter = (IVisionAdapter<TData>)Activator.CreateInstance(adapterTypeSource.Type);
            Name = Adapter.Name;

            _ea.GetEvent<DataEvent>().Subscribe(ProcessDataFromDataEvent);
            _ea.GetEvent<BoundaryItemsRequestEvent>().Subscribe(adapterName =>
            {
                if (adapterName == Adapter.Name)
                {
                    SendBoundariesToInProcessAlc(EventBehavior.Response);
                }
            });

            ShowPropertiesCommand = new DelegateCommand(ShowProperties);
            RunSingleFileCommand = new DelegateCommand(RunSingleFile);
            RunFolderCommand = new DelegateCommand(RunFolder);
            OpenSettingDialogCommand = new DelegateCommand(OpenSettingDialog);
            OpenWeightEditorDialogCommand = new DelegateCommand(OpenWeightEditorDialog);
            ClearDisplayDataCommand = new DelegateCommand(() =>
            {
                RawData = new ObservableCollection<DynamicClass>();
                WeightedData = new ObservableCollection<DynamicClass>();
                ReservedData = new ObservableCollection<DynamicClass>();
            });
            SwitchDisplayDataCommand = new DelegateCommand<string>(typeText =>
            {
                var success = Enum.TryParse<OutputDataType>(typeText, out var t);
                if (!success) throw new Exception();
                var selectedData = t == OutputDataType.Raw ? RawData :
                    t == OutputDataType.Weighted ? WeightedData : ReservedData;
                DisplayData = selectedData;
            });
            OpenBoundarySettingDialogCommand = new DelegateCommand(() =>
            {
                _dialogService.ShowDialog("BoundarySettingDialog", new DialogParameters()
                {
                    {"login", DistributedVisionRunnerModule.Privileges.CanUpdateBoundaries},
                    {"unitName", Adapter.Name},
                    {"dialogService", dialogService},
                    {"rawBoundaries", RawBoundaries},
                    {"weightedBoundaries", WeightedBoundaries},
                    {"reservedBoundaries", ReservedBoundaries},
                    {"SaveValidator", DistributedVisionRunnerModule.SaveBoundariesValidator}
                }, dr =>
                {
                    if (dr.Result == ButtonResult.OK)
                    {
                        ReloadBoundaries();
                        SendBoundariesToInProcessAlc(EventBehavior.ChangeNotification);
                    }
                });
            });

            _userSetting = LoadUserSetting(Adapter.Name);
            ShouldShowTable = _userSetting.ShouldShowTable;
            ClearOutDatedImages();
            UpdateImagesLayoutFromSetting();


            _noDelimiterProblems = CheckForImageFileNameDelimiterProblems();

            _noNamingProblems = CheckNamingProblems();
            if (Processor.EnableWeighting && _noNamingProblems)
            {
                WeightsConfigured = CheckIfWeightsAreConfigured(Adapter.Name);
                if (WeightsConfigured) ReloadWeights();
            }

            if (_noNamingProblems) CreateDatabase();

            CreateVariableNamesCache();

            // Setup server
            var currentProcessName = Process.GetCurrentProcess().ProcessName;
            EnableZeroMQ = currentProcessName == "DistributedVisionRunner.App";

            if (EnableZeroMQ)
            {
                ServerAddress = ConfigurationManager.AppSettings[$"ServerAddress-{Adapter.Name}"] ??
                                Adapter.ZeroMQAddress ?? "tcp://localhost:6000";
                _serverSocket = new ResponseSocket(ServerAddress);
                new Thread(ListenForZeroMqRequests) { IsBackground = true }.Start();
            }

            // Load processor's files
            _processorConfigFileDir = Path.Combine(Constants.AppDataDir, $"{Name}/ProcessorConfigs");
            Directory.CreateDirectory(_processorConfigFileDir);
            Dictionary<string, FileError> fileErrors = null;

            try
            {
                fileErrors = Processor.LoadFiles(_processorConfigFileDir);
            }
            catch (Exception e)
            {
                Log($"加载视觉配置文件时出错: {e.Message} \n {e.StackTrace}",
                    $"Error when loading vision config files: {e.Message} \n {e.StackTrace}", LogLevel.Fatal);
                _visionConfigFilesLoadFailed = true;
            }

            if (fileErrors != null && fileErrors.Any())
            {
                _visionConfigFilesLoadFailed = true;
                // Remove directory part if any
                var fileAndErrors = fileErrors.Select(p => new { FileName = Path.GetFileName(p.Key), Error = p.Value })
                    .ToArray();
                // Find out the longest file name for string padding
                var longestFileName = fileAndErrors.OrderBy(o => o.FileName.Length).Last().FileName;
                // Format fail message
                var failedMessage = string.Join("\n",
                    fileAndErrors.Select(p => $"{p.FileName.PadRight(longestFileName.Length)}: {p.Error}"));

                Log($"加载以下视觉配置文件时失败:\n {failedMessage} ",
                    $"Failed to load the following vision config files:\n {failedMessage} ", LogLevel.Fatal);
            }

            ReloadBoundaries();

            // Set data grid column bindings
            RegenerateDataGridBindings(_userSetting.DoubleFieldFormat);

        }

        private void RegenerateDataGridBindings(string doubleFieldFormat)
        {
            if (doubleFieldFormat == "None") doubleFieldFormat = null;
            //Raw
            {
                var rawDataColumnBindings = new List<ColumnBinding>
                    {
                        new ColumnBinding() {Header = "时间", IsReadOnly = true, Path = "Time"},
                        new ColumnBinding() {Header = "穴位", IsReadOnly = true, Path = "Cavity"},
                        new ColumnBinding() {Header = "SN", IsReadOnly = true, Path = "SN"},
                    };

                foreach (var outputName in Processor.RawOutputNames)
                {
                    rawDataColumnBindings.Add(new ColumnBinding()
                    {
                        Header = outputName,
                        Path = outputName,
                        IsReadOnly = true,
                        StringFormat = doubleFieldFormat
                    });
                }

                RawDataColumnBindings = rawDataColumnBindings.ToArray();
            }

            // Reserved
            {
                var reservedDataColumnBindings = new List<ColumnBinding>
                    {
                        new ColumnBinding() {Header = "时间", IsReadOnly = true, Path = "Time"},
                        new ColumnBinding() {Header = "穴位", IsReadOnly = true, Path = "Cavity"},
                        new ColumnBinding() {Header = "SN", IsReadOnly = true, Path = "SN"},
                    };

                if (Adapter.ReservedOutputNames.HasValue)
                {
                    var floatNames = Adapter.ReservedOutputNames.Value.floatNames;
                    if (floatNames != null)
                    {
                        foreach (var outputName in floatNames)
                        {
                            reservedDataColumnBindings.Add(new ColumnBinding()
                            {
                                Header = outputName,
                                Path = outputName,
                                IsReadOnly = true,
                                StringFormat = doubleFieldFormat
                            });
                        }
                    }

                    var integerNames = Adapter.ReservedOutputNames.Value.integerNames;
                    if (integerNames != null)
                    {
                        foreach (var outputName in integerNames)
                        {
                            reservedDataColumnBindings.Add(new ColumnBinding()
                            {
                                Header = outputName,
                                Path = outputName,
                                IsReadOnly = true,
                            });
                        }
                    }

                    var textNames = Adapter.ReservedOutputNames.Value.textNames;
                    if (textNames != null)
                    {
                        foreach (var outputName in textNames)
                        {
                            reservedDataColumnBindings.Add(new ColumnBinding()
                            {
                                Header = outputName,
                                Path = outputName,
                                IsReadOnly = true,
                            });
                        }
                    }

                    ReservedDataColumnBindings = reservedDataColumnBindings.ToArray();
                }
            }

            // Weighted
            {
                var weightedDataColumnBindings = new List<ColumnBinding>
                    {
                        new ColumnBinding() {Header = "时间", IsReadOnly = true, Path = "Time"},
                        new ColumnBinding() {Header = "穴位", IsReadOnly = true, Path = "Cavity"},
                        new ColumnBinding() {Header = "SN", IsReadOnly = true, Path = "SN"},
                    };

                foreach (var outputName in Processor.WeightedOutputNames)
                {
                    weightedDataColumnBindings.Add(new ColumnBinding()
                    {
                        Header = outputName,
                        Path = outputName,
                        IsReadOnly = true,
                        StringFormat = doubleFieldFormat
                    });
                }

                WeightedDataColumnBindings = weightedDataColumnBindings.ToArray();
            }
        }

        /// <summary>
        /// Delimiter in any group of Adapter.ImageFileNamingRule, for example, ProductSectionDelimiter, must be unique to other group.
        /// Otherwise, file name parsing will conflict.
        /// </summary>
        /// <returns>True if no problem</returns>
        private bool CheckForImageFileNameDelimiterProblems()
        {
            if (Adapter.ImageFileNamingRule == null) return true;

            var delimiterPairs = new List<(char start, char end)>()
            {
                Adapter.ImageFileNamingRule.ProductSectionDelimiter,
                Adapter.ImageFileNamingRule.CavitySectionDelimiter,
                Adapter.ImageFileNamingRule.CreationTimeSectionDelimiter,
                Adapter.ImageFileNamingRule.SnSectionDelimiter,
            };

            foreach (var currentPair in delimiterPairs)
            {
                var remainingPairs = delimiterPairs.Except(new[] { currentPair });
                var delimiterOfRemainingPairs = new HashSet<char>();
                foreach (var pair in remainingPairs)
                {
                    delimiterOfRemainingPairs.Add(pair.start);
                    delimiterOfRemainingPairs.Add(pair.end);
                }

                if (delimiterOfRemainingPairs.Contains(currentPair.start))
                {
                    Log($"分隔符'{currentPair.start}同其余组存在冲突'", $"Delimiter '{currentPair.start}' is conflicting with other groups", LogLevel.Fatal);
                    return false;
                }
                if (delimiterOfRemainingPairs.Contains(currentPair.end))
                {
                    Log($"分隔符'{currentPair.end}同其余组存在冲突'", $"Delimiter '{currentPair.end}' is conflicting with other groups", LogLevel.Fatal);
                    return false;
                }
            }

            return true;
        }

        #endregion ctor

        #region api

        public void Stop()
        {
            _serverSocket?.Close();
        }

        #endregion api

        #region impl

        private void SendBoundariesToInProcessAlc(EventBehavior eventBehavior)
        {
            _ea.GetEvent<BoundaryItemsRespondEvent>().Publish((Adapter.Name, eventBehavior, new Dictionary<OutputDataType, List<BoundaryItem>>()
            {
                [OutputDataType.Raw] = RawBoundaries,
                [OutputDataType.Weighted] = WeightedBoundaries,
                [OutputDataType.Reserved] = ReservedBoundaries
            }));
        }

        private void GenBoundaryLookupTable(List<BoundaryItem> boundaryItems, ref Dictionary<string, BoundaryItem> boundariesLookupTable)
        {
            boundariesLookupTable = boundaryItems.ToDictionary(i => i.Name, i => i);
        }


        private void ReloadBoundaries()
        {
            RawBoundaries = Helpers.GetBoundaries(Adapter.Name, OutputDataType.Raw, Processor.RawOutputNames);
            WeightedBoundaries = Helpers.GetBoundaries(Adapter.Name, OutputDataType.Weighted, Processor.WeightedOutputNames);

            if (Adapter.ReservedOutputNames != null)
                ReservedBoundaries = Helpers.GetBoundaries(Adapter.Name, OutputDataType.Reserved,
                    Adapter.ReservedOutputNames.Value.floatNames);
        }

        private bool CheckNamingProblems()
        {
            // Check naming of Adapter.ReservedOutputNames
            if (Adapter.ReservedOutputNames != null)
            {
                var reservedOutputNames = Adapter.ReservedOutputNames.Value;
                if (reservedOutputNames.floatNames != null)
                    foreach (var name in reservedOutputNames.floatNames)
                    {
                        if (!VariableNameIsValid(name, "Adapter")) return false;
                    }

                if (reservedOutputNames.integerNames != null)
                {
                    foreach (var name in reservedOutputNames.integerNames)
                    {
                        if (!VariableNameIsValid(name, "Adapter")) return false;
                    }

                    if (reservedOutputNames.integerNames.Contains("Cavity"))
                    {
                        Log("Adapter.ReservedOutputNames.integerNames不能含有Cavity", "Adapter.ReservedOutputNames.integerNames can not contains cavity", LogLevel.Fatal);
                        return false;
                    }
                }

                if (reservedOutputNames.textNames != null)
                {
                    foreach (var name in reservedOutputNames.textNames)
                    {
                        if (!VariableNameIsValid(name, "Adapter")) return false;
                    }

                    if (reservedOutputNames.textNames.Contains("SN"))
                    {
                        Log("Adapter.ReservedOutputNames.textNames不能含有SN", "Adapter.ReservedOutputNames.textNames can not contains SN", LogLevel.Fatal);
                        return false;
                    }
                }
            }



            // Check process.RawOutputNames
            if (Processor.RawOutputNames == null || Processor.RawOutputNames.Length == 0)
            {
                Log("Processor.RawOutputNames未定义",
                    "Processor.RawOutputNames has not been properly defined", LogLevel.Fatal);
                return false;
            }

            foreach (var name in Processor.RawOutputNames)
            {
                if (!VariableNameIsValid(name, "Processor")) return false;
            }


            if (Processor.EnableWeighting)
            {

                if (Processor.WeightNames == null || Processor.WeightNames.Length == 0)
                {
                    Log("Processor.WeightNames未定义",
                        "Processor.WeightNames has not been properly defined", LogLevel.Fatal);
                    return false;
                }
                foreach (var name in Processor.WeightNames)
                {
                    if (!VariableNameIsValid(name, "Processor")) return false;
                }

                if (Processor.WeightedOutputNames == null || Processor.WeightedOutputNames.Length == 0)
                {
                    Log("Processor.WeightedOutputNames未定义",
                        "Processor.WeightedOutputNames has not been properly defined", LogLevel.Fatal);
                    return false;
                }
                foreach (var name in Processor.WeightedOutputNames)
                {
                    if (!VariableNameIsValid(name, "Processor")) return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Create variable names cache for validation during data output
        /// </summary>
        private void CreateVariableNamesCache()
        {
            if (Adapter.ReservedOutputNames != null)
            {
                var reservedOutputNames = Adapter.ReservedOutputNames.Value;
                CreateNamesCache(reservedOutputNames.floatNames, ref _adapterFloatNamesLookup);
                CreateNamesCache(reservedOutputNames.integerNames, ref _adapterIntegerNamesLookup);
                CreateNamesCache(reservedOutputNames.textNames, ref _adapterTextNamesLookup);
            }

            CreateNamesCache(Processor.RawOutputNames, ref _processorOutputNamesLookup);
        }

        private static void CreateNamesCache(string[] names, ref HashSet<string> lookup)
        {
            if (names == null || names.Length == 0) return;
            lookup = new HashSet<string>(names);
        }

        private bool CheckIfWeightsAreConfigured(string adapterName)
        {
            // Check for adapter and processor definition errors

            if (Adapter.WeightSetCount < 1)
            {
                Log("Adapter.WeightSetCount未正确定义",
                    "Adapter.WeightSetCount has not been properly defined", LogLevel.Fatal);
                return false;
            }


            _weightProjectDir = Path.Combine(Constants.AppDataDir, $"{adapterName}/WeightSettings");
            Directory.CreateDirectory(_weightProjectDir);
            _weightProjectFilePath = Path.Combine(_weightProjectDir, $"{adapterName}.uws");

            if (!File.Exists(_weightProjectFilePath))
            {
                Log("权重未配置", "Weights not configured", LogLevel.Fatal);
                return false;
            }

            // Check weight files
            var (loadedWeights, newlyAddedWeights) = Helpers.LoadWeights(_weightProjectDir, Processor.WeightNames, Adapter.WeightSetCount);
            if (newlyAddedWeights != null && newlyAddedWeights.Any())
            {
                var weightNamesText = string.Join(", ", newlyAddedWeights);
                Log($"存在未配置的权重:{weightNamesText}", $"Weights that are not set: {weightNamesText}", LogLevel.Fatal);
                return false;
            }

            // Check method files
            var (loadedMethods, missingMethods) = Helpers.LoadMethods(_weightProjectDir, Processor.WeightedOutputNames);
            if (missingMethods.Any())
            {
                var missingMethodsText = string.Join(", ", missingMethods);
                Log($"存在未配置的权重方法:{missingMethodsText}", $"Methods that are not set: {missingMethodsText}", LogLevel.Fatal);
                return false;
            }

            // Try run weights
            var firstSetOfWeights = loadedWeights[0].WeightItems.ToDictionary(item => item.Name, item => item.Weight);
            var scriptOutputAndExpressions =
                loadedMethods.ToDictionary(m => m.OutputName, m => m.MethodDefinition);
            var random = new Random(42);
            var testInputs = Processor.RawOutputNames.ToDictionary(name => name, name => random.NextDouble());

            var (output, exceptions) = WeightWeaver.Weight(testInputs, firstSetOfWeights, scriptOutputAndExpressions);

            if (exceptions.Count == 0) return true;

            if (exceptions.Values.All(e => e is DivideByZeroException))
            {
                Log("试运行计算时出现DivideByZeroException", "DivideByZeroException occurred while trying to run weights", LogLevel.Warn);
                return true;
            }

            // Show exception details
            var exceptionDetails = new List<string>();
            foreach (var outputName in exceptions.Keys)
            {
                var exception = exceptions[outputName];
                exceptionDetails.Add($"{outputName}: [{exception.GetType()}] {exception.Message}");
            }

            var exceptionDetailsText = string.Join("\n", exceptionDetails);
            Log($"试运行时出错: \n{exceptionDetailsText}", $"Error occurs while trying to run weights: \n{exceptionDetailsText}", LogLevel.Fatal);

            return false;
        }

        private bool VariableNameIsValid(string name, string adapterOrProcessor)
        {
            if (!_namingPattern.IsMatch(name))
            {
                Log($"{adapterOrProcessor}的变量{name}不符合命名规范",
                    $"Variable {name} from {adapterOrProcessor} does not match naming convention", LogLevel.Fatal);
                return false;
            }

            return true;
        }

        private void OpenWeightEditorDialog()
        {
            _dialogService.ShowDialog("WeightEditorDialog", new DialogParameters{{"Constraint", new WeightConfigurationConstraint()
            {
                ProjectFilePath = _weightProjectFilePath,
                InputNames = Processor.RawOutputNames,
                OutputNames = Processor.WeightedOutputNames,
                WeightNames = Processor.WeightNames,
                WeightSetCount = Adapter.WeightSetCount,
                TabName = Adapter.Name
            } },
                { "Login", DistributedVisionRunnerModule.Privileges.CanUpdateWeights },
                {"SaveValidator", DistributedVisionRunnerModule.SaveWeightsValidator}

            }, r =>
            {
                if (r.Result == ButtonResult.OK)
                {
                    WeightsConfigured = true;
                    ReloadWeights();
                }
            });
        }

        private void ReloadWeights()
        {
            var (loadedWeightCollections, missingWeightNames) =
                Helpers.LoadWeights(_weightProjectDir, Processor.WeightNames, Adapter.WeightSetCount);
            _weightCollectionByCavity = loadedWeightCollections.ToDictionary(c => c.Index,
                c => c.WeightItems.ToDictionary(i => i.Name, i => i.Weight));

            var (loadedMethods, missingMethodNames) =
                Helpers.LoadMethods(_weightProjectDir, Processor.WeightedOutputNames);
            _methodsByOutputName = loadedMethods.ToDictionary(m => m.OutputName, m => m.MethodDefinition);
        }

        private void OpenSettingDialog()
        {
            var settingCopied = MiscHelper.CopyObject(_userSetting);
            _dialogService.ShowDialog("UserSettingDialog",
                new DialogParameters { { "setting", settingCopied }, { "login", DistributedVisionRunnerModule.Privileges.CanUpdateGeneralSettings }, { "visionConfigDir", _processorConfigFileDir } }, r =>
                {
                    if (r.Result == ButtonResult.OK)
                    {
                        var newSetting = r.Parameters.GetValue<ProcessUnitUserSetting>("setting");
                        if (newSetting.DoubleFieldFormat != _userSetting.DoubleFieldFormat)
                            RegenerateDataGridBindings(newSetting.DoubleFieldFormat);

                        _userSetting = newSetting;
                        SaveUserSetting(_userSetting);


                        Log("保存设置成功", "Save setting success");
                    }
                    else
                    {
                        Log("设置未保存", "Settings not save", LogLevel.Warn);
                    }
                });
        }

        private void SaveUserSetting(ProcessUnitUserSetting setting)
        {
            using (var writer = new StreamWriter(_userSettingPath))
            {
                var serializer = new XmlSerializer(typeof(ProcessUnitUserSetting));
                serializer.Serialize(writer, setting);
            }
        }

        private void CreateDatabase()
        {
            var projectName = Adapter.ProjectName ?? "VisionRunner";
            _tableNameRawOnline = $"{projectName}.{Adapter.Name}_Raw_Online";
            _tableNameWeightedOnline = $"{projectName}.{Adapter.Name}_Weighted_Online";
            _tableNameReservedOnline = $"{projectName}.{Adapter.Name}_Reserved_Online";

            _tableNameRawOffline = $"{projectName}.{Adapter.Name}_Raw_Offline";
            _tableNameWeightedOffline = $"{projectName}.{Adapter.Name}_Weighted_Offline";
            _tableNameReservedOffline = $"{projectName}.{Adapter.Name}_Reserved_Offline";


            var integerNamesRaw = new[] { "Cavity" };
            var textNamesRaw = new[] { "SN" };


            try
            {
                var proxy = new CygiaSqliteAccessProxy(SqliteAccessServiceClient.EndpointConfiguration.NetTcpBinding_ISqliteAccessService);

                // Create raw tables
                proxy.CreateTable(_tableNameRawOnline, Processor.RawOutputNames, integerNamesRaw, textNamesRaw);
                proxy.CreateTable(_tableNameRawOffline, Processor.RawOutputNames, integerNamesRaw, textNamesRaw);

                // Create weighted tables
                if (Processor.EnableWeighting)
                {
                    var integerNamesWeighted = new[] { "Cavity" };
                    var textNamesWeighted = new[] { "SN" };
                    proxy.CreateTable(_tableNameWeightedOnline, Processor.WeightedOutputNames, integerNamesWeighted, textNamesWeighted);
                    proxy.CreateTable(_tableNameWeightedOffline, Processor.WeightedOutputNames, integerNamesWeighted, textNamesWeighted);
                }

                // Create reserved tables
                if (Adapter.ReservedOutputNames != null)
                {
                    var reservedNames = Adapter.ReservedOutputNames.Value;
                    var integerNamesReserved = new List<string> { "Cavity" };
                    var textNamesReserved = new List<string> { "SN" };

                    if (reservedNames.integerNames != null)
                    {
                        integerNamesReserved.AddRange(reservedNames.integerNames);
                    }

                    if (reservedNames.textNames != null)
                    {
                        textNamesReserved.AddRange(reservedNames.textNames);
                    }

                    proxy.CreateTable(_tableNameReservedOnline, reservedNames.floatNames, integerNamesReserved.ToArray(), textNamesReserved.ToArray());
                    proxy.CreateTable(_tableNameReservedOffline, reservedNames.floatNames, integerNamesReserved.ToArray(), textNamesReserved.ToArray());
                }


            }
            catch (System.ServiceModel.EndpointNotFoundException)
            {
                _databaseServiceInstalled = false;
                Log("未安装服务CygiaSqliteAccess.Host.exe, \n请访问https://gitee.com/believingheart/cygia-sqlite-access-service/releases下载安装",
                    "CygiaSqliteAccess.Host.exe not installed", LogLevel.Warn);
            }
        }

        private ProcessUnitUserSetting LoadUserSetting(string name)
        {
            _userSettingDir = Path.Combine(Constants.AppDataDir, $"{name}");
            Directory.CreateDirectory(_userSettingDir);

            _userSettingPath = Path.Combine(_userSettingDir, "UserSettings.xml");

            if (!File.Exists(_userSettingPath))
            {
                return new ProcessUnitUserSetting() { ImageSaveMainFolder = GetDefaultImageSaveDir(name) };
            }

            using (var reader = new StreamReader(_userSettingPath))
            {
                var serializer = new XmlSerializer(typeof(ProcessUnitUserSetting));
                return serializer.Deserialize(reader) as ProcessUnitUserSetting;
            }
        }

        private async void RunFolder()
        {
            var dir = FileSystemHelper.GetDirFromDialog(cacheFile: Path.Combine(Constants.AppDataDir, $"Cache/{Adapter.Name}.RunFolder.RecentFolder"));
            if (!string.IsNullOrEmpty(dir))
            {
                var allFiles = Directory.GetFiles(dir);
                var filesThatMatch = allFiles;
                if (Adapter.ImageFileFilter.HasValue) filesThatMatch = allFiles.
                        Where(f => Adapter.ImageFileFilter.Value.extensions.Any(ex => Path.GetExtension(f).Replace(".", "").ToUpper() == ex.ToUpper())).ToArray();

                if (filesThatMatch.Length == 0)
                {
                    Warn("没有找到符合格式的文件");
                    return;
                }

                Log($"读取{filesThatMatch.Length}个文件", $"{filesThatMatch.Length} files are read");
                foreach (var file in filesThatMatch)
                {
                    await ProcessDataFromFile(file);
                }
            }
        }

        private async void RunSingleFile()
        {
            var selectedFile = FileSystemHelper.GetFileFromDialog(pattern: Adapter.ImageFileFilter, cacheFile: Path.Combine(Constants.AppDataDir, $"Cache/{Adapter.Name}.RunSingleFile.RecentFolder"));
            if (!string.IsNullOrEmpty(selectedFile))
            {
                await ProcessDataFromFile(selectedFile);
            }
        }

        private async Task ProcessDataFromFile(string filePath)
        {
            var fileName = Path.GetFileName(filePath);
            Log($"正在离线运行图片{fileName}", $"Running image offline: {fileName}");
            var (cavity, sn, productType) = ParseCavitySNProductType(Path.GetFileNameWithoutExtension(filePath));

            try
            {
                var data = Adapter.ReadFile(filePath);
                await Task.Run(() => ProcessData(data, productType, cavity, sn, DateTime.Now, DataSourceType.DataFile, Guid.NewGuid()));
            }
            catch (FileNotFoundException e)
            {
                Log($"图像文件缺失, 运行图像文件失败: {e.Message}", $"Got FileNotFoundException ex for {filePath}. Detail: {e.Message}", LogLevel.Fatal);
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="fileNameWithExt">
        /// Standard pattern : {sn}_Cavity{cavity}[{productType}]
        /// </param>
        /// <returns></returns>
        private (int cavity, string sn, string productType) ParseCavitySNProductType(string fileNameWithExt)
        {
            var fileName = Path.GetFileNameWithoutExtension(fileNameWithExt);
            var namingRule = Adapter.ImageFileNamingRule ?? ImageFileNamingRule.Default;
            // Extract sn
            var snExtractor = new NamingSectionParser(namingRule.SnSectionDelimiter);
            var snSection = snExtractor.Parse(fileName);
            var snParseSuccess = snSection != null;
            var sn = snSection ?? UnspecifiedSN;

            // Extract cavity
            var cavityPattern = new Regex(@"Cavity(\d+)");
            var cavityExtractor = new NamingSectionParser(namingRule.CavitySectionDelimiter);
            var cavitySection = cavityExtractor.Parse(fileName);
            int cavityExtracted = 1;
            var cavityParseSuccess = cavitySection != null;
            if (cavityParseSuccess)
            {
                cavityParseSuccess = cavityPattern.IsMatch(fileName);
                if (cavityParseSuccess)
                {
                    cavityExtracted = int.Parse(cavityPattern.Match(fileName).Groups[1].Value);
                }
            }

            // Extract product type
            var productTypeExtractor = new NamingSectionParser(namingRule.ProductSectionDelimiter);
            var productTypeText = productTypeExtractor.Parse(fileName);
            var productTypeParseSuccess = productTypeText != null;
            productTypeText = productTypeParseSuccess
                ? productTypeText
                : Adapter.DefaultProductType ?? UnspecifiedProductName;

            // Warn if any parse not success
            if (!snParseSuccess || !cavityParseSuccess || !productTypeParseSuccess)
            {
                var snWarn = snParseSuccess ? string.Empty : "SN";
                var cavityWarn = cavityParseSuccess ? string.Empty : "Cavity";
                var productTypeWarn = productTypeParseSuccess ? string.Empty : "ProductType";
                var warnItems = string.Join(", ", new[] { snWarn, cavityWarn, productTypeWarn }.Where(t => !string.IsNullOrEmpty(t)));
                Log($"以下文件名信息解析失败: {warnItems}", $"Parsing failures occurs for: {warnItems}", LogLevel.Warn);
            }

            return (cavityExtracted, sn, productTypeText);
        }

        private void ShowProperties()
        {
            var dialogParams = new DialogParameters {
                { "ServerAddress", ServerAddress},
                { "AdapterAssembly", _adapterTypeSource.AssemblyFilePath},
                { "AdapterType", _adapterTypeSource.TypeName},
                {"AdapterVersion", FileSystemHelper.GetAssemblyVersionByType(_adapterTypeSource.Type)},
                { "ProcessorAssembly", _processorTypeSource.AssemblyFilePath},
                { "ProcessorType", _processorTypeSource.TypeName},
                {"ProcessorVersion", Processor.Version?? FileSystemHelper.GetAssemblyVersionByType(_processorTypeSource.Type)},
                {"CoreVersion", FileSystemHelper.GetAssemblyVersionByType(typeof(IVisionClient))},
                {"ModuleVersion", FileSystemHelper.GetAssemblyVersionByType(typeof(ProcessUnitUserSetting))},
            };

            _dialogService.ShowDialog("VisionProcessUnitPropertyDialog", dialogParams, r => { });
        }

        private void ListenForZeroMqRequests()
        {
            Log($"启动ZeroMQ服务器({ServerAddress})", $"Started ZeroMQ server at {ServerAddress}");
            try
            {
                _zeroMQServerIsListeningBlocker = new ManualResetEvent(false);
                while (true)
                {
                    NetMQMessage message = null;
                    _serverSocket.TryReceiveMultipartMessage(TimeSpan.FromMilliseconds(500), ref message);
                    if (_isShuttingDown)
                    {
                        return;
                    }

                    if (message == null)
                    {
                        continue;
                    }

                    var messageType = message[0].ConvertToString();

                    switch (messageType)
                    {
                        case "ProcessRequest":
                            {
                                var productType = message[1].ConvertToString();
                                var sn = message[2].ConvertToString();

                                var cavity = int.Parse(message[3].ConvertToString());
                                var timestampText = message[4].ConvertToString();
                                var timeStamp = string.IsNullOrEmpty(timestampText)
                                    ? DateTime.Now
                                    : DateTime.ParseExact(timestampText, "yyyy-MMdd-HH-mm-ss-fff",
                                        new DateTimeFormatInfo());
                                var data = message[5].Buffer;
                                ProcessData(Adapter.ConvertInput(data), productType, cavity, sn, timeStamp, DataSourceType.ZeroMQ, Guid.NewGuid());
                                break;
                            }
                        case "BoundaryItemsRequest":
                            {
                                var json = JsonConvert.SerializeObject(new BoundaryItemsCollection()
                                {
                                    RawBoundaries = RawBoundaries,
                                    WeightedBoundaries = WeightedBoundaries,
                                    ReservedBoundaries = ReservedBoundaries
                                });
                                _serverSocket.SendFrame(json);
                                break;
                            }
                        default:
                            throw new KeyNotFoundException($"Can not handle such request: {messageType}");
                    }

                }
            }
            finally
            {
                _zeroMQServerIsListeningBlocker.Set();
            }
        }

        private void ProcessDataFromDataEvent((byte[] data, string productType, int cavity, string sn, DateTime? timestamp, Guid guid, string adapterName) input)
        {
            if (input.adapterName == Adapter.Name)
            {
                new Thread(() => ProcessData(Adapter.ConvertInput(input.data), input.productType, input.cavity, input.sn, input.timestamp ?? DateTime.Now, DataSourceType.DataEvent, input.guid)
                    )
                { IsBackground = true }.Start();
            }
        }

        private void ProcessData(List<TData[]> data, string productType, int cavity, string inputSn,
            DateTime timestamp,
            DataSourceType dataSource, Guid guid)
        {
            var assertionFailed = false;
            var weightsNotConfiguredProperly = Processor.EnableWeighting && !WeightsConfigured;
            if (weightsNotConfiguredProperly)
            {
                Log("权重未分配, 数据处理无法进行", "Weights not configured, cancel processing", LogLevel.Fatal);
                assertionFailed = true;
            }

            if (!_noNamingProblems)
            {
                Log("存在变量命名问题, 数据处理无法进行", "Naming problems found, cancel processing", LogLevel.Fatal);
                assertionFailed = true;
            }

            if (cavity < 1 || cavity > Adapter.WeightSetCount)
            {
                Log($"Cavity{cavity} 不是有效的穴位, 数据处理无法进行, \n有效的Cavity范围是[1, {Adapter.WeightSetCount}]", $"Cavity{cavity} is illegal cavity", LogLevel.Fatal);
                assertionFailed = true;
            }

            if (_visionConfigFilesLoadFailed)
            {
                Log("视觉配置文件未成功加载, 数据处理无法进行", "Vision config files didn't load properly, cancel processing", LogLevel.Fatal);
                assertionFailed = true;
            }

            if (!_noDelimiterProblems)
            {
                Log("图像文件名的分隔符设置存在问题", "There are problems with image file name delimiter settings in VisionAdapter", LogLevel.Fatal);
                assertionFailed = true;
            }

            if (assertionFailed)
            {
                if (dataSource != DataSourceType.DataFile) SendErrorToALC(dataSource, guid);
                return;
            }

            IsIdle = false;
            BusyCounter++;
            try
            {
                var sn = string.IsNullOrWhiteSpace(inputSn) ? UnspecifiedSN : inputSn;
                productType = string.IsNullOrWhiteSpace(productType)
                    ? Adapter.DefaultProductType ?? UnspecifiedProductName
                    : productType;
                Log($"正在处理来自{dataSource}, 长度为{data.Count}, 夹具编号为{cavity}的数据, sn为{sn}",
                    $"Start processing data of length({data.Count}) of cavity({cavity}), sn({sn}) from data source({dataSource})");

                var stopwatch = Stopwatch.StartNew();
                ProcessResult result = null;
                try
                {
                    result = Processor.Process(data, productType, cavity);
                }
                catch (Exception ex)
                {
                    // Save image file
                    var exDetail = $"{ex.GetType()} \n{ex.Message}\n {ex.StackTrace}";
                    if (dataSource != DataSourceType.DataFile)
                        SaveImage(data, productType, cavity, "ERROR", sn, ex, timestamp);
                    Log($"视觉处理出现异常: {exDetail}", $"Errored during vision processing: {exDetail}", LogLevel.Fatal);

                    // Report ex to ALC
                    if (dataSource != DataSourceType.DataFile)
                    {
                        SendErrorToALC(dataSource, guid);
                    }

                    var rawDisplay = new List<GraphicPayload>();
                    for (int imageIndex = 0; imageIndex < Adapter.GraphicMetaDatas.Length; imageIndex++)
                    {
                        GraphicPayload displayItem = null;
                        var metaData = Adapter.GraphicMetaDatas[imageIndex];
                        var currentDataType = typeof(TData);
                        if (currentDataType == typeof(byte))
                        {
                            var imageData = data[imageIndex] as byte[];
                            displayItem = new GraphicPayload(new ImageData<byte>(imageData), metaData.Width,
                                metaData.Height, metaData.RawChannels, null);
                        }
                        else if (currentDataType == typeof(float))
                        {
                            var imageData = data[imageIndex] as float[];
                            displayItem = new GraphicPayload(new ImageData<float>(imageData), metaData.Width,
                                metaData.Height, metaData.RawChannels, null);
                        }
                        else if (currentDataType == typeof(float))
                        {
                            var imageData = data[imageIndex] as float[];
                            displayItem = new GraphicPayload(new ImageData<float>(imageData), metaData.Width,
                                metaData.Height, metaData.RawChannels, null);
                        }

                        rawDisplay.Add(displayItem);
                    }

                    DisplayImages = rawDisplay;

                    return;
                }

                void RoundNumbers(Dictionary<string, double> dataDict)
                {
                    if (dataDict == null) return;

                    if (_userSetting.DoubleFieldFormat == "None") return;

                    double RoundNumber(double input)
                    {
                        if (double.IsNaN(input)) return input;

                        return double.Parse(input.ToString(_userSetting.DoubleFieldFormat));
                    }

                    foreach (var key in dataDict.Keys.ToArray())
                    {
                        var value = dataDict[key];
                        dataDict[key] = RoundNumber(value);
                    }
                }

                var rawResults = result.Statistics;
                var rawData = rawResults.FloatResults;
                RoundNumbers(rawData);
                // Check if processor give results that match what it promised
                // at the first run

                var namesAreEqual = CompareNames(_processorOutputNamesLookup, rawData.Keys.ToArray());
                if (!namesAreEqual)
                {
                    Log($"Processor的输出数据种类({ConcatStrings(rawData.Keys.ToArray())})与其定义的不相符,\n 请检查类的定义和输出类型",
                        $"The result of processor does not match what it promised", LogLevel.Fatal);
                    if (dataSource != DataSourceType.DataFile) SendErrorToALC(dataSource, guid);
                    return;
                }

                Dictionary<string, double> weightedDict = null;

                if (Processor.EnableWeighting)
                {
                    weightedDict = Weight(rawResults.FloatResults, cavity);

                    RoundNumbers(weightedDict);
                }



                var rawDoubleData = GetDoubleDataWithBoundaryChecking(rawResults?.FloatResults, _rawBoundariesLookupTable);
                var rawStatistics = new StatisticsResults(rawDoubleData, rawResults.IntegerResults, rawResults.TextResults);

                var weightedDoubleData = GetDoubleDataWithBoundaryChecking(weightedDict, _weightedBoundariesLookupTable);


                var (resultType, reservedResults, errType, errMessage) = Adapter.GetResultTypeAndReservedOutputs(rawStatistics, weightedDoubleData);

                RoundNumbers(reservedResults?.FloatResults);

                var ms = stopwatch.ElapsedMilliseconds;
                Log($"计算耗时{ms}ms", $"Data process finished in {ms} ms");

                // Check if adapter give results that match what it promised
                // at the first run
                if (Adapter.ReservedOutputNames != null)
                {
                    var promiseNamesAndActualNames = new List<(string kind, HashSet<string> promiseNames, string[] actualNames)>
                    {
                        ("AdapterReservedFloatNames",_adapterFloatNamesLookup, reservedResults.FloatResults.Keys.ToArray()),
                        ("AdapterReservedIntegerNames",_adapterIntegerNamesLookup, reservedResults.IntegerResults?.Keys.ToArray()),
                        ("AdapterReservedTextNames",_adapterTextNamesLookup, reservedResults.TextResults?.Keys.ToArray())
                    };

                    foreach (var (kind, promiseNames, actualNames) in promiseNamesAndActualNames)
                    {
                        namesAreEqual = CompareNames(promiseNames, actualNames);
                        if (!namesAreEqual)
                        {
                            Log($"{kind} 的输出数据种类({ConcatStrings(actualNames)})与其定义的不相符,\n 请检查类的定义和输出类型",
                                $"The result of {kind} does not match what it promised", LogLevel.Fatal);
                            if (dataSource != DataSourceType.DataFile) SendErrorToALC(dataSource, guid);
                            return;
                        }
                    }
                }

                if (dataSource != DataSourceType.DataFile) ReportResult(rawStatistics, weightedDoubleData, reservedResults, resultType, dataSource, guid);

                DisplayStatisticResults(rawResults, weightedDict, reservedResults, cavity, sn, timestamp);

                DisplayImages = result.DisplayData;

                // Save non-error image file
                if (dataSource != DataSourceType.DataFile && _userSetting.ImageSaveFilter != ImageSaveFilter.ErrorOnly)
                {
                    if (!(_userSetting.ImageSaveFilter == ImageSaveFilter.ErrorAndNg && resultType == ResultType.OK))
                    {
                        var subFolder = string.Empty;
                        if (_userSetting.ImageSaveSchema == ImageSaveSchema.OkNgInOneFolder)
                        {
                            subFolder = "OkAndNg";
                        }
                        else
                        {
                            subFolder = resultType.ToString();
                        }

                        SaveImage(data, productType, cavity, subFolder, sn, null, timestamp);
                    }
                }

                // Save error image. Note that this Error is classified by Adapter.GetResultTypeAndReservedOutputs
                if (dataSource != DataSourceType.DataFile && resultType == ResultType.ERROR)
                {
                    SaveImage(data, productType, cavity, "CustomError", sn, new CustomProcessingException(errType ?? "UnknownErrorType", errMessage ?? "UnknownErrorMessage"), timestamp);
                }

                // Write to database
                if (_databaseServiceInstalled)
                {
                    // If save any data
                    if (_userSetting.SaveRawDataOffline || _userSetting.SaveRawDataOnline ||
                        _userSetting.SaveWeightedDataOffline || _userSetting.SaveWeightedDataOnline
                        || _userSetting.SaveReservedDataOffline || _userSetting.SaveReservedDataOnline)
                    {
                        var proxy = new CygiaSqliteAccessProxy(SqliteAccessServiceClient.EndpointConfiguration.NetTcpBinding_ISqliteAccessService);
                        // If save raw data
                        if (_userSetting.SaveRawDataOffline || _userSetting.SaveRawDataOnline)
                        {
                            var integerFields = new[] { new IntegerField() { Name = "Cavity", Value = cavity } };
                            var textFields = new[] { new TextField() { Name = "SN", Value = sn } };
                            var rowDatas = new[]
                            {
                                new RowData
                                {
                                    CreationTime = timestamp,
                                    DoubleFields = rawData.Select(p => new DoubleField {Name = p.Key, Value = p.Value})
                                        .ToArray(),
                                    IntegerFields = integerFields,
                                    TextFields = textFields
                                }
                            };

                            if (_userSetting.SaveRawDataOffline && dataSource == DataSourceType.DataFile)
                            {
                                Log("保存离线原始数据", "Saved offline raw data");
                                InsertToDb(proxy, _tableNameRawOffline, rowDatas);
                            }

                            if (_userSetting.SaveRawDataOnline && dataSource != DataSourceType.DataFile)
                            {
                                Log("保存在线原始数据", "Saved online raw data");
                                InsertToDb(proxy, _tableNameRawOnline, rowDatas);
                            }
                        }

                        // If save weighted data
                        if (Processor.EnableWeighting)
                        {
                            if (_userSetting.SaveWeightedDataOffline || _userSetting.SaveWeightedDataOnline)
                            {
                                var integerFields = new[] { new IntegerField { Name = "Cavity", Value = cavity } };
                                var textFields = new[] { new TextField { Name = "SN", Value = sn } };

                                var rowDatas = new[]
                                {
                                new RowData
                                {
                                    CreationTime = timestamp,
                                    DoubleFields = weightedDict
                                        .Select(p => new DoubleField {Name = p.Key, Value = p.Value}).ToArray(),
                                    IntegerFields = integerFields,
                                    TextFields = textFields
                                }
                            };

                                if (_userSetting.SaveWeightedDataOffline && dataSource == DataSourceType.DataFile)
                                {
                                    Log("保存离线补偿后数据", "Saved offline weighted data");
                                    InsertToDb(proxy, _tableNameWeightedOffline, rowDatas);
                                }

                                if (_userSetting.SaveWeightedDataOnline && dataSource != DataSourceType.DataFile)
                                {
                                    Log("保存在线补偿后数据", "Saved online weighted data");
                                    InsertToDb(proxy, _tableNameWeightedOnline, rowDatas);
                                }
                            }
                        }


                        // If save reserved data
                        if (Adapter.ReservedOutputNames != null)
                        {
                            if (_userSetting.SaveReservedDataOffline || _userSetting.SaveReservedDataOnline)
                            {
                                var integerFields = reservedResults.IntegerResults
                                    .Select(p => new IntegerField { Name = p.Key, Value = p.Value }).ToList();
                                integerFields.Insert(0, new IntegerField { Name = "Cavity", Value = cavity });
                                var textFields = reservedResults.TextResults
                                    .Select(p => new TextField { Name = p.Key, Value = p.Value }).ToList();
                                textFields.Insert(0, new TextField { Name = "SN", Value = sn });

                                var rowDatas = new[]
                                {
                                    new RowData
                                    {
                                        CreationTime = timestamp,
                                        DoubleFields = reservedResults.FloatResults
                                            .Select(p => new DoubleField {Name = p.Key, Value = p.Value}).ToArray(),
                                        IntegerFields = integerFields.ToArray(),
                                        TextFields = textFields.ToArray()
                                    }
                                };

                                if (_userSetting.SaveReservedDataOffline && dataSource == DataSourceType.DataFile)
                                {
                                    Log("保存离线预留数据", "Saved offline weighted data");
                                    InsertToDb(proxy, _tableNameReservedOffline, rowDatas);
                                }

                                if (_userSetting.SaveReservedDataOnline && dataSource != DataSourceType.DataFile)
                                {
                                    Log("保存在线预留数据", "Saved online weighted data");
                                    InsertToDb(proxy, _tableNameReservedOnline, rowDatas);
                                }
                            }
                        }
                    }
                }
            }
            finally
            {
                BusyCounter--;
                if (BusyCounter == 0)
                {
                    IsIdle = true;
                }
            }
        }



        private void InsertToDb(CygiaSqliteAccessProxy proxy, string table, RowData[] rowDatas)
        {
            lock (_dbLocker)
            {
                proxy.Insert(table, rowDatas);
            }
        }

        private static List<DoubleData> GetDoubleDataWithBoundaryChecking(Dictionary<string, double> measuredValues, Dictionary<string, BoundaryItem> boundaryItems) =>
             measuredValues?.Select(fr =>
                 {
                     var boundary = boundaryItems[fr.Key];
                     var value = fr.Value;
                     var outOfBoundary = boundary.Enable && (double.IsNaN(value) || value < boundary.Floor || value > boundary.Ceiling);
                     return new DoubleData()
                     {
                         BoundCheckingSuccess = !outOfBoundary,
                         Ceiling = boundary.Ceiling,
                         Floor = boundary.Floor,
                         EnableBoundChecking = boundary.Enable,
                         Name = fr.Key,
                         Value = value
                     };
                 })
                 .ToList();


        private static string ConcatStrings(string[] array)
        {
            if (array == null || array.Length == 0) return string.Empty;
            return string.Join(",", array);
        }

        private void SaveImage(List<TData[]> data, string productType, int cavity, string subFolder, string sn, Exception exception,
            DateTime dateTime)
        {
            var dir = Path.Combine(ImageSaveFolderToday, subFolder);
            var namingRule = Adapter.ImageFileNamingRule ?? ImageFileNamingRule.Default;

            string WrapSection((char start, char end) sectionDelimiter, object content)
            {
                return $"{sectionDelimiter.start}{content}{sectionDelimiter.end}";
            }

            Directory.CreateDirectory(dir);
            var snPart = WrapSection(namingRule.SnSectionDelimiter, sn);

            var productTypePart = WrapSection(namingRule.ProductSectionDelimiter, productType);

            var cavityPart = WrapSection(namingRule.CavitySectionDelimiter, $"Cavity{cavity}");

            var dateTimePart = WrapSection(namingRule.CreationTimeSectionDelimiter,
                dateTime.ToString("MMdd-HHmm-ss-fff"));

            var fileNameWithoutExt = $"{snPart}{productTypePart}{cavityPart}{dateTimePart}";

            Adapter.SaveImage(data, ImageSaveFolderToday, subFolder, fileNameWithoutExt, exception);
        }

        private void SendErrorToALC(DataSourceType dataSourceType, Guid guid)
        {
            if (dataSourceType == DataSourceType.ZeroMQ)
            {
                var json = JsonConvert.SerializeObject(new DistributedVisionRunnerResultPack() { ResultType = ResultType.ERROR });
                _serverSocket.SendFrame(json);
            }
            else if (dataSourceType == DataSourceType.DataEvent)
            {
                _ea.GetEvent<VisionResultEvent>().Publish((new DistributedVisionRunnerResultPack() { ResultType = ResultType.ERROR }, guid));
            }
        }

        /// <summary>
        /// Compare two array of names
        /// </summary>
        /// <param name="promiseNames"></param>
        /// <param name="actualNames"></param>
        /// <returns>
        /// true if all matched
        /// </returns>
        private bool CompareNames(HashSet<string> promiseNames, string[] actualNames)
        {
            if (promiseNames == null || promiseNames.Count == 0)
            {
                if (actualNames == null || actualNames.Length == 0)
                {
                    return true;
                }
                return false;
            }
            else
            {
                if (actualNames == null || actualNames.Length == 0) return false;
                if (actualNames.Length != promiseNames.Count) return false;

                return actualNames.All(promiseNames.Contains);
            }
        }

        private Dictionary<string, double> Weight(Dictionary<string, double> inputFloats, int cavity)
        {
            var selectedWeightCollection = _weightCollectionByCavity[cavity];

            var testInputs = inputFloats.ToDictionary(p => p.Key, p => p.Value);

            var (output, exceptions) = WeightWeaver.Weight(testInputs, selectedWeightCollection, _methodsByOutputName);

            return output.ToDictionary(p => p.Key, p => p.Value);
        }

        private static string GetDefaultImageSaveDir(string tabName)
        {
            // Determine log dir
            // Log dir sits at the first drive that is larger than 100 GB behind c drive
            var drives = DriveInfo.GetDrives();

            var gb100 = 100_000_000_000;
            var storageDrive = drives.FirstOrDefault(d => !d.Name.StartsWith("C") && d.TotalSize > gb100)?.Name;
            var subDir = $"DistributedVisionRunnerImages\\{tabName}";

            if (storageDrive == null) return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), subDir);

            return Path.Combine(storageDrive, subDir);
        }

        private void DisplayStatisticResults(Statistics rawResults, Dictionary<string, double> weightedDict,
            Statistics reservedResults, int cavity,
            string sn, DateTime dateTime)
        {
            var rowsToDeleteIfOverflow = 50;
            var overflowRowCount = 100;
            var displayDateTime = dateTime.ToString("HH:mm:ss fff");
            // Display raw data
            {
                var rawDataFields = new List<Field>()
                {
                    new Field()
                    {
                        FieldName = "Time",
                        FieldType = typeof(string)
                    },

                    new Field()
                    {
                        FieldName = "Cavity",
                        FieldType = typeof(int)
                    },

                    new Field()
                    {
                        FieldName = "SN",
                        FieldType = typeof(string)
                    }

                };



                foreach (var name in Processor.RawOutputNames)
                {
                    rawDataFields.Add(new Field()
                    {
                        FieldName = name,
                        FieldType = typeof(double)
                    });
                }

                var rawDataOneRow = new DynamicClass(rawDataFields)
                {
                };

                Dynamic.InvokeSet(rawDataOneRow, "Time", displayDateTime);
                Dynamic.InvokeSet(rawDataOneRow, "Cavity", cavity);
                Dynamic.InvokeSet(rawDataOneRow, "SN", sn);

                if (rawResults.FloatResults != null)
                {
                    foreach (var item in rawResults.FloatResults)
                    {
                        Dynamic.InvokeSet(rawDataOneRow, item.Key, item.Value);
                    }
                }


                Application.Current.Dispatcher.InvokeAsync(() =>
                {
                    RawData.Add(rawDataOneRow);
                    if (RawData.Count > overflowRowCount)
                        RawData = new ObservableCollection<DynamicClass>(RawData.Skip(rowsToDeleteIfOverflow));
                });
            }

            // Display reserved data
            {
                if (reservedResults != null)
                {
                    var reservedDataFields = new List<Field>()
                    {
                        new Field()
                        {
                            FieldName = "Time",
                            FieldType = typeof(string)
                        },

                        new Field()
                        {
                            FieldName = "Cavity",
                            FieldType = typeof(int)
                        },

                        new Field()
                        {
                            FieldName = "SN",
                            FieldType = typeof(string)
                        }

                    };


                    var floatNames = Adapter.ReservedOutputNames.Value.floatNames;
                    if (floatNames != null)
                    {
                        foreach (var name in floatNames)
                        {
                            reservedDataFields.Add(new Field()
                            {
                                FieldName = name,
                                FieldType = typeof(double)
                            });
                        }
                    }

                    var integerNames = Adapter.ReservedOutputNames.Value.integerNames;
                    if (integerNames != null)
                    {
                        foreach (var name in integerNames)
                        {
                            reservedDataFields.Add(new Field()
                            {
                                FieldName = name,
                                FieldType = typeof(int)
                            });
                        }
                    }

                    var textNames = Adapter.ReservedOutputNames.Value.textNames;
                    if (textNames != null)
                    {
                        foreach (var name in textNames)
                        {
                            reservedDataFields.Add(new Field()
                            {
                                FieldName = name,
                                FieldType = typeof(string)
                            });
                        }
                    }

                    var reservedDataOneRow = new DynamicClass(reservedDataFields)
                    {
                    };

                    Dynamic.InvokeSet(reservedDataOneRow, "Time", displayDateTime);
                    Dynamic.InvokeSet(reservedDataOneRow, "Cavity", cavity);
                    Dynamic.InvokeSet(reservedDataOneRow, "SN", sn);



                    if (reservedResults.FloatResults != null)
                    {
                        foreach (var item in reservedResults.FloatResults)
                        {
                            Dynamic.InvokeSet(reservedDataOneRow, item.Key, item.Value);
                        }
                    }

                    if (reservedResults.IntegerResults != null)
                    {
                        foreach (var item in reservedResults.IntegerResults)
                        {
                            Dynamic.InvokeSet(reservedDataOneRow, item.Key, item.Value);
                        }
                    }

                    if (reservedResults.TextResults != null)
                    {
                        foreach (var item in reservedResults.TextResults)
                        {
                            Dynamic.InvokeSet(reservedDataOneRow, item.Key, item.Value);
                        }
                    }


                    Application.Current.Dispatcher.InvokeAsync(() =>
                    {
                        ReservedData.Add(reservedDataOneRow);
                        if (ReservedData.Count > overflowRowCount)
                            ReservedData =
                                new ObservableCollection<DynamicClass>(
                                    ReservedData.Skip(rowsToDeleteIfOverflow));
                    });
                }
            }

            // Display weighted data
            {
                if (weightedDict != null)
                {
                    var weightedFields = new List<Field>()
                    {
                        new Field()
                        {
                            FieldName = "Time",
                            FieldType = typeof(string)
                        },

                        new Field()
                        {
                            FieldName = "Cavity",
                            FieldType = typeof(int)
                        },

                        new Field()
                        {
                            FieldName = "SN",
                            FieldType = typeof(string)
                        }

                    };



                    foreach (var name in Processor.WeightedOutputNames)
                    {
                        weightedFields.Add(new Field()
                        {
                            FieldName = name,
                            FieldType = typeof(double)
                        });
                    }

                    var weightedDataOneRow = new DynamicClass(weightedFields)
                    {
                    };

                    Dynamic.InvokeSet(weightedDataOneRow, "Time", displayDateTime);
                    Dynamic.InvokeSet(weightedDataOneRow, "Cavity", cavity);
                    Dynamic.InvokeSet(weightedDataOneRow, "SN", sn);

                    foreach (var item in weightedDict)
                    {
                        Dynamic.InvokeSet(weightedDataOneRow, item.Key, item.Value);
                    }


                    Application.Current.Dispatcher.InvokeAsync(() =>
                    {
                        WeightedData.Add(weightedDataOneRow);
                        if (WeightedData.Count > overflowRowCount)
                            WeightedData =
                                new ObservableCollection<DynamicClass>(
                                    WeightedData.Skip(rowsToDeleteIfOverflow));
                    });
                }
            }

        }


        private static byte[] ScaleToDisplayRange(short[] inputArray, float min, float max)
        {
            var output = new byte[inputArray.Length];
            var range = max - min;
            for (int index = 0; index < inputArray.Length; index++)
            {
                var absValue = inputArray[index] - min;
                var scaledValue = (byte)(absValue / range * 255);
                output[index] = scaledValue;
            }

            return output;
        }

        private static byte[] ScaleToDisplayRange(ushort[] inputArray, float min, float max)
        {
            var output = new byte[inputArray.Length];
            var range = max - min;
            for (int index = 0; index < inputArray.Length; index++)
            {
                var absValue = inputArray[index] - min;
                var scaledValue = (byte)(absValue / range * 255);
                output[index] = scaledValue;
            }

            return output;
        }

        private static byte[] ScaleToDisplayRange(float[] inputArray, float min, float max)
        {
            var output = new byte[inputArray.Length];
            var range = max - min;
            for (int index = 0; index < inputArray.Length; index++)
            {
                var absValue = inputArray[index] - min;
                var scaledValue = (byte)(absValue / range * 255);
                output[index] = scaledValue;
            }

            return output;
        }

        private WriteableBitmap CreateDisplayImage(int width, int height, byte[] pixelData)
        {
            var writeableBitmap = new WriteableBitmap(width, height, 96.0, 96.0, PixelFormats.Rgb24, BitmapPalettes.Halftone256Transparent);
            writeableBitmap.WritePixels(new Int32Rect(0, 0, width, height), pixelData, width * 3, 0);
            return writeableBitmap;
        }

        private void UpdateDisplayImage(WriteableBitmap image, byte[] rgbData, int width, int height)
        {
            image.WritePixels(new Int32Rect(0, 0, width, height), rgbData, width * 3, 0);
        }

        private void ReportResult(StatisticsResults rawResults, List<DoubleData> weightedResults,
            Statistics reservedResults, ResultType resultType, DataSourceType dataSource, Guid guid)
        {


            var reservedDoubleData =
                GetDoubleDataWithBoundaryChecking(reservedResults?.FloatResults, _reservedBoundariesLookupTable);
            var reservedStatistics = new StatisticsResults(reservedDoubleData, reservedResults?.IntegerResults, reservedResults?.TextResults);

            var resultPack = new DistributedVisionRunnerResultPack() { RawResults = rawResults, ReservedResults = reservedStatistics, ResultType = resultType, WeightedResults = weightedResults };

            if (dataSource == DataSourceType.DataEvent) _ea.GetEvent<VisionResultEvent>().Publish((resultPack, guid));
            else if (dataSource == DataSourceType.ZeroMQ)
            {
                // Serialize rawResults
                var json = JsonConvert.SerializeObject(resultPack);
                _serverSocket.SendFrame(json);
            }
            Log("发送计算结果", "Reported statistic results");
        }

        private void Log(string displayMessage, string saveMessage, LogLevel logLevel = LogLevel.Info)
        {
            DistributedVisionRunnerModule.Log(new LogItem($"({Adapter.Name}): {displayMessage}", $"VisionPage({Adapter.Name}): {saveMessage}") { LogLevel = logLevel });
        }

        private void Warn(string message)
        {
            _dialogService.ShowDialog("VisionRunnerNotificationDialog", new DialogParameters { { "message", message } }, r => { });
        }


        #endregion impl

        public void RemoveSettings()
        {
            var dirOfTheTab = $"{Constants.AppDataDir}/{Name}";

            try
            {
                Helpers.DeleteDirectory(dirOfTheTab);
            }
            catch (Exception e)
            {
                Log($"删除配置文件夹失败,\n 请手动删除文件夹: {dirOfTheTab}\n{e.Message}", $"Delete config of the tab failed,\n please manually remove: {dirOfTheTab}\n{e.Message}");
            }
        }



        public void ShutdownZeroMQServerIfAny()
        {
            _isShuttingDown = true;

            if (EnableZeroMQ)
            {
                Log("正在关闭ZeroMQ服务器...", "Shutting down ZeroMQ server...");
                _zeroMQServerIsListeningBlocker?.WaitOne();
                _serverSocket?.Close();
            }
        }

        public void ClearOutDatedImages()
        {
            try
            {
                var mainImageDir = _userSetting.ImageSaveMainFolder;
                Directory.CreateDirectory(mainImageDir);
                var today = DateTime.Now;
                var datesToProtect = new List<DateTime>();
                for (int i = 0; i < _userSetting.DaysToKeepImages; i++)
                {
                    datesToProtect.Add(today - TimeSpan.FromDays(i));
                }

                var dirNamesToProtect = new HashSet<string>(datesToProtect.Select(d => d.ToString("yyyy-MM-dd")));
                var allImageDirs = Directory.GetDirectories(mainImageDir);
                var dirsToDelete =
                    allImageDirs.Where(dir => !dirNamesToProtect.Contains(Path.GetFileNameWithoutExtension(dir))).ToArray();

                foreach (var dir in dirsToDelete)
                {
                    Helpers.DeleteDirectory(dir);
                }
            }
            catch
            {
                // Don't care
            }
        }
    }

    public class VisionProcessUnitByte : VisionProcessUnitViewModel<byte>
    {
        public VisionProcessUnitByte(IEventAggregator ea, IDialogService dialogService, TypeSource processorTypeSource, TypeSource adapterTypeSource) : base(ea, dialogService, processorTypeSource, adapterTypeSource)
        {
        }
    }

    public class VisionProcessUnitFloat : VisionProcessUnitViewModel<float>
    {
        public VisionProcessUnitFloat(IEventAggregator ea, IDialogService dialogService, TypeSource processorTypeSource, TypeSource adapterTypeSource) : base(ea, dialogService, processorTypeSource, adapterTypeSource)
        {
        }
    }

    public class VisionProcessUnitShort : VisionProcessUnitViewModel<short>
    {
        public VisionProcessUnitShort(IEventAggregator ea, IDialogService dialogService, TypeSource processorTypeSource, TypeSource adapterTypeSource) : base(ea, dialogService, processorTypeSource, adapterTypeSource)
        {
        }
    }

    public class VisionProcessUnitUShort : VisionProcessUnitViewModel<ushort>
    {
        public VisionProcessUnitUShort(IEventAggregator ea, IDialogService dialogService, TypeSource processorTypeSource, TypeSource adapterTypeSource) : base(ea, dialogService, processorTypeSource, adapterTypeSource)
        {
        }
    }
}